/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.flowable.common.engine.impl.calendar;

import java.util.Date;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;

import org.flowable.common.engine.api.FlowableIllegalArgumentException;
import org.flowable.common.engine.impl.runtime.ClockReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An BusinessCalendar implementation for cycle based schedules that takes into account a different daylight savings time zone than the one that the server is configured for.
 * <p>
 * For CRON strings DSTZONE is used as the time zone that the CRON schedule refers to. Leave it out to use the server time zone.
 * <p>
 * For ISO strings the time zone offset for the date/time specified is part of the string itself. DSTZONE is used to determine what the offset should be NOW, which may be different than when the
 * workflow was scheduled if it is scheduled to run across a DST event.
 * 
 * <pre>
 *   For example:
 *      R/2013-10-01T20:30:00/P1D DSTZONE:US/Eastern
 *      R/2013-10-01T20:30:00/P1D DSTZONE:UTC
 *      R/2013-10-01T20:30:00/P1D DSTZONE:US/Arizona
 *      0 30 20 ? * MON,TUE,WED,THU,FRI * DSTZONE:US/Eastern
 *      0 30 20 ? * MON,TUE,WED,THU,FRI * DSTZONE:UTC
 *      0 30 20 ? * MON,TUE,WED,THU,FRI * DSTZONE:US/Arizona
 * </pre>
 * 
 * Removing the DSTZONE key will cause to use the server's time zone. This is the original behavior.
 * <p>
 * Schedule strings are versioned. Version 1 strings will use the original CycleBusinessCalendar implementation. All new properties are ignored. Version 2 strings will use the new daylight saving time
 * logic.
 * 
 * <pre>
 *   For example:
 *      R/2013-10-01T20:30:00/P1D VER:2 DSTZONE:US/Eastern
 *      0 30 20 ? * MON,TUE,WED,THU,FRI * VER:1 DSTZONE:US/Arizona
 * </pre>
 * 
 * By default (if no VER key is included in the string), it assumes version 2. This can be changed by modifying the defaultScheduleVersion property.
 * <p>
 * 
 * @author mseiden
 */
public class AdvancedCycleBusinessCalendar extends CycleBusinessCalendar {

    private Integer defaultScheduleVersion;

    private static final Integer DEFAULT_VERSION = 2;

    private static final Logger LOGGER = LoggerFactory.getLogger(AdvancedCycleBusinessCalendar.class);

    private static final Map<Integer, AdvancedSchedulerResolver> resolvers;

    static {
        resolvers = new ConcurrentHashMap<>();
        resolvers.put(1, new AdvancedSchedulerResolverWithoutTimeZone());
        resolvers.put(2, new AdvancedSchedulerResolverWithTimeZone());
    }

    public AdvancedCycleBusinessCalendar(ClockReader clockReader) {
        super(clockReader);
    }

    public AdvancedCycleBusinessCalendar(ClockReader clockReader, Integer defaultScheduleVersion) {
        this(clockReader);
        this.defaultScheduleVersion = defaultScheduleVersion;
    }

    public Integer getDefaultScheduleVersion() {
        return defaultScheduleVersion == null ? DEFAULT_VERSION : defaultScheduleVersion;
    }

    public void setDefaultScheduleVersion(Integer defaultScheduleVersion) {
        this.defaultScheduleVersion = defaultScheduleVersion;
    }

    @Override
    public Date resolveDuedate(String duedateDescription, int maxIterations) {
        LOGGER.info("Resolving Due Date: {}", duedateDescription);

        String timeZone = getValueFrom("DSTZONE", duedateDescription);
        String version = getValueFrom("VER", duedateDescription);

        // START is a legacy value that is no longer used, but may still exist in deployed job schedules
        // Could be used in the future as a start date for a CRON job
        // String startDate = getValueFrom("START", duedateDescription);

        duedateDescription = removeValueFrom("VER", removeValueFrom("START", removeValueFrom("DSTZONE", duedateDescription))).trim();

        try {
            LOGGER.info("Base Due Date: {}", duedateDescription);

            Date date = resolvers.get(version == null ? getDefaultScheduleVersion() : Integer.valueOf(version)).resolve(duedateDescription, clockReader,
                    timeZone == null ? clockReader.getCurrentTimeZone() : TimeZone.getTimeZone(timeZone));

            LOGGER.info("Calculated Date: {}", date == null ? "Will Not Run Again" : date);

            return date;

        } catch (Exception e) {
            throw new FlowableIllegalArgumentException("Cannot parse duration", e);
        }

    }

    private String getValueFrom(String field, String duedateDescription) {
        int fieldIndex = duedateDescription.indexOf(field + ":");

        if (fieldIndex > -1) {
            int nextWhiteSpace = duedateDescription.indexOf(' ', fieldIndex);

            fieldIndex += field.length() + 1;

            if (nextWhiteSpace > -1) {
                return duedateDescription.substring(fieldIndex, nextWhiteSpace);
            } else {
                return duedateDescription.substring(fieldIndex);
            }
        }

        return null;
    }

    private String removeValueFrom(String field, String duedateDescription) {
        int fieldIndex = duedateDescription.indexOf(field + ":");

        if (fieldIndex > -1) {
            int nextWhiteSpace = duedateDescription.indexOf(' ', fieldIndex);

            if (nextWhiteSpace > -1) {
                return duedateDescription.replace(duedateDescription.substring(fieldIndex, nextWhiteSpace), "");
            } else {
                return duedateDescription.substring(0, fieldIndex);
            }
        }

        return duedateDescription;
    }
}
